/**
 * Copyright 2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xume.raob.core;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.xume.raob.ObjectBuilder;
import org.xume.raob.annotations.PostBuild;
import org.xume.raob.annotations.BuildStepFactory;
import org.xume.raob.annotations.BuildStepFactoryClass;
import org.xume.raob.exceptions.ObjectBuilderConfigurationException;
import org.xume.raob.util.ArrayUtils;
import org.xume.raob.util.ReflectionUtils;

/**
 * @author Johan Siebens
 */
@SuppressWarnings("unchecked")
public class IntrospectionResult {

	private static final Map<Class<?>, IntrospectionResult> CACHE = new HashMap<Class<?>, IntrospectionResult>();

	public static IntrospectionResult forBuilder(Class<? extends ObjectBuilder<?>> builderInterface) {
		IntrospectionResult result = CACHE.get(builderInterface);
		if (result == null) {
			Class<?> targetClass = ReflectionUtils.targetClass(builderInterface);
			result = new IntrospectionResult(targetClass);

			for (Method method : builderInterface.getMethods()) {
				if (!ReflectionUtils.isBuildMethod(method)) {

					PostBuild postBuild = method.getAnnotation(PostBuild.class);
					if (postBuild != null) {
						PostBuildMethod postBuildMethod = postBuildMethod(postBuild, method);
						result.postBuildMethods.put(method, postBuildMethod);
						continue;
					}

					if (builderInterface.isAssignableFrom(method.getReturnType())) {
						Annotation[] annotations = method.getAnnotations();
						for (Annotation annotation : annotations) {
							Class<? extends Annotation> annotationType = annotation.annotationType();
							BuildStepFactoryClass factoryClassAnnotation = annotationType.getAnnotation(BuildStepFactoryClass.class);
							if (factoryClassAnnotation != null) {
								BuildStepFactory buildStepFactory = ReflectionUtils.createInstance(factoryClassAnnotation.value());
								buildStepFactory.initialize(annotation, method, result.targetClass);
								result.buildStepFactories.put(method, buildStepFactory);
							}
						}
					} else {
						throw new ObjectBuilderConfigurationException("Methods declared in [" + builderInterface.getSimpleName()
								+ "] should return an instance of [" + builderInterface.getSimpleName()
								+ "] or should be annotated as @PostBuild");
					}
				}
			}

			CACHE.put(builderInterface, result);
		}
		return result;
	}

	private static PostBuildMethod postBuildMethod(PostBuild postBuild, Method method) {
		Class<?>[] parameterTypes = method.getParameterTypes();
		Class<?>[] actualParameterTypes = (Class<?>[]) ArrayUtils.addFirst(parameterTypes, Object.class);
		try {
			Method postBuildMethod = postBuild.value().getMethod(method.getName(), actualParameterTypes);
			Object target = ReflectionUtils.createInstance(postBuild.value());
			return new PostBuildMethod(postBuildMethod, target);
		} catch (NoSuchMethodException e) {
			throw new ObjectBuilderConfigurationException("No match found for method [" + method + "] in @PostBuild class ["
					+ postBuild.getClass() + "]", e);
		}
	}

	private Map<Method, PostBuildMethod> postBuildMethods;

	private Map<Method, BuildStepFactory<?>> buildStepFactories;

	private Class<?> targetClass;

	private IntrospectionResult(Class<?> targetClass) {
		this.targetClass = targetClass;
		this.postBuildMethods = new HashMap<Method, PostBuildMethod>();
		this.buildStepFactories = new HashMap<Method, BuildStepFactory<?>>();
	}

	public PostBuildMethod getPostBuildMethod(Method method) {
		return this.postBuildMethods.get(method);
	}

	public BuildStepFactory<?> getBuildStepFactory(Method method) {
		return this.buildStepFactories.get(method);
	}

	public Class<?> getTargetClass() {
		return targetClass;
	}

	public boolean isPostBuildMethod(Method method) {
		return this.postBuildMethods.containsKey(method);
	}

	public boolean isBuildStep(Method method) {
		return this.buildStepFactories.containsKey(method);
	}

}
